﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Xml.Linq;
using Red.Controls.Utils;
using Red.Core;
using Red.Core.Extensions;

namespace Red.Controls.Tile
{
    public class CustomObservableCollection<T> : ObservableCollection<T>
    {
        #region Public Methods

        /// <summary>
        /// A way of adding an item to an observable collection without triggering the notification event
        /// </summary>
        public void AddSilent( T item )
        {
            Items.Add( item );
        }

        #endregion Public Methods
    }

    class Comparer : IEqualityComparer<Tile>
    {
        public bool Equals( Tile x, Tile y )
        {
            return x.Id == y.Id;
        }

        public int GetHashCode( Tile obj )
        {
            return obj.Function.GetHashCode();
        }
    }

    [ContentProperty( "Contents" )]
    public class Container : ContentControl, IDragSource, IDragTarget
    {
        #region Private Data

        private LayoutGuide _layout = new LayoutGuide();
        private Grid _grid = new Grid();

        private int _mouseRowOffset;
        private int _mouseColumnOffset;

        private List<TileImpl> _displacedTiles = new List<TileImpl>();
        private int _previousDragRow = -1;
        private int _previousDragColumn = -1;

        private int _temporaryRowsAddedByAnimation = 0;

        private Callback _clickCallback = new Callback();
        private Callback _doubleClickCallback = new Callback();


        private AggregateStringProvider<Tile.IStrings> _stringProvider = new AggregateStringProvider<Tile.IStrings>();
        private IStringProvider<Tile.IStrings> _mainTileStringProvider;

        #endregion Private Data

        #region Dependency Properties

        public static readonly DependencyProperty ColumnsProperty =
        DependencyProperty.Register( "Columns", typeof( int ), typeof( Container ), new PropertyMetadata( SetColumns ) );

        public static readonly DependencyProperty RowsProperty =
        DependencyProperty.Register( "Rows", typeof( int ), typeof( Container ), new PropertyMetadata( SetRows ) );

        public static readonly DependencyProperty ColumnWidthProperty =
        DependencyProperty.Register( "ColumnWidth", typeof( GridLength ), typeof( Container ) );

        public static readonly DependencyProperty RowHeightProperty =
        DependencyProperty.Register( "RowHeight", typeof( GridLength ), typeof( Container ) );

        public static readonly DependencyProperty IconSizeIncrementProperty =
        DependencyProperty.Register( "IconSizeIncrement", typeof( double ), typeof( Container ), new PropertyMetadata( 32.0 ) );

        public static readonly DependencyProperty TooltipDescriptionProperty =
        DependencyProperty.Register( "TooltipDescription", typeof( string ), typeof( Container ) );

        public static readonly DependencyProperty TooltipOpacityProperty =
        DependencyProperty.Register( "TooltipOpacity", typeof( double ), typeof( Container ) );

        #endregion Dependency Properties

        #region Public Properties

        public double MinVersion { get; set; }

        public int Columns
        {
            get { return (int)GetValue( ColumnsProperty ); }
            set { SetValue( ColumnsProperty, value ); }
        }

        public int Rows
        {
            get { return (int)GetValue( RowsProperty ); }
            set { SetValue( RowsProperty, value ); }
        }

        public GridLength ColumnWidth
        {
            get { return (GridLength)GetValue( ColumnWidthProperty ); }
            set { SetValue( ColumnWidthProperty, value ); }
        }

        public GridLength RowHeight
        {
            get { return (GridLength)GetValue( RowHeightProperty ); }
            set { SetValue( RowHeightProperty, value ); }
        }

        public double IconSizeIncrement
        {
            get { return (double)GetValue( IconSizeIncrementProperty ); }
            set { SetValue( IconSizeIncrementProperty, value ); }
        }

        public string TooltipDescription
        {
            get { return (string)GetValue( TooltipDescriptionProperty ); }
            set { SetValue( TooltipDescriptionProperty, value ); }
        }

        public double TooltipOpacity
        {
            get { return (double)GetValue( TooltipOpacityProperty ); }
            set { SetValue( TooltipOpacityProperty, value ); }
        }

        public CustomObservableCollection<Tile> Contents { get; private set; } = new CustomObservableCollection<Tile>();
        public Utils.DragDrop DragDrop { get; private set; } = new Utils.DragDrop();
        public Reserve Reserve { get; private set; }
        public IList Hidden => Reserve.Tiles;

        public AggregateStringProvider<Tile.IStrings> StringProvider => _stringProvider;

        public string StringProviderSource
        {
            set
            {
                _mainTileStringProvider = new XmlStringProvider<Tile.Strings>( value, "Tile" );
                _stringProvider.Providers.Add( _mainTileStringProvider );
            }
        }

        private string _userPresetsLocation;

        public string UserPresetsLocation
        {
            get { return _userPresetsLocation; }
            set
            {
                if ( _userPresetsLocation != value )
                {
                    _userPresetsLocation = value;

                    XmlDataProvider source = TryFindResource( "UserPresetMenuSource" ) as XmlDataProvider;

                    if ( source != null )
                    {
                        source.Source = new Uri( _userPresetsLocation );
                    }
                }
            }
        }

        public Dictionary<string, Tile> VersionRoleModel = new Dictionary<string, Tile>();

        #endregion Public Properties

        #region C-Tor

        public Container()
        {
            Resources.MergedDictionaries.Add( new ResourceDictionary() { Source = new Uri( "pack://application:,,,/Red.Controls;component/Tile/TileResources.xaml" ) } );

            CommandBinding loadPrefix = new CommandBinding( (ICommand)TryFindResource( "Tile.Preset.Load" ), LoadPreset );
            CommandBindings.Add( loadPrefix );

            CommandBinding savePrefix = new CommandBinding( (ICommand)TryFindResource( "Tile.Preset.Save" ), SavePreset );
            CommandBindings.Add( savePrefix );

            Grid wrapper = new Grid();

            StackPanel content = new StackPanel();
            TextBlock tooltipDescription = new TextBlock() { TextAlignment = TextAlignment.Right, Margin = new Thickness( 3 ), Height = 16 };
            tooltipDescription.SetBinding( TextBlock.TextProperty, new Binding( "TooltipDescription" ) { Source = this } );
            tooltipDescription.SetBinding( OpacityProperty, new Binding( "TooltipOpacity" ) { Source = this } );
            tooltipDescription.SetResourceReference( TextBlock.ForegroundProperty, "GrayBrush1" );

            WrapPanel bottom = new WrapPanel() { HorizontalAlignment = HorizontalAlignment.Right };

            HidingButton reserveButton = new HidingButton()
            {
                Width = 40,
                Height = 18,
                RevealingElement = this
            };

            reserveButton.Content = new RedIcon() { Canvas = TryFindResource( "appbar_chevron_down" ) as Canvas, Height = 12, Width = 24 };
            bottom.Children.Add( reserveButton );

            content.Children.Add( tooltipDescription );
            content.Children.Add( _grid );

            wrapper.Children.Add( content );
            wrapper.Children.Add( bottom );

            wrapper.Loaded += ( object sender, RoutedEventArgs e ) =>
            {
                double height = content.ActualHeight;
                bottom.RenderTransform = new TranslateTransform() { Y = height };
                HeightChanged += ( int difference, double sizeDifference ) => { height += sizeDifference;  bottom.RenderTransform = new TranslateTransform() { Y = height }; };
            };

            Content = wrapper;
            Contents.CollectionChanged += Contents_CollectionChanged;
            AllowDrop = true;
            DragDrop.Initialize( this );
            DragDrop.RegisterTarget( this );

            _grid.SetBinding( BackgroundProperty, new Binding( "Background" ) { Source = this } );
            Reserve = new Reserve( this ) { Callback = _clickCallback };
            Reserve.DragDrop.RegisterTarget( this );
            reserveButton.Click += ( object sender, RoutedEventArgs e ) => { if( reserveButton.Opacity > 0 ) Reserve.Show(); };
            reserveButton.MouseEnter += ( object sender, MouseEventArgs e ) => { if ( reserveButton.Opacity > 0 ) ShowTooltip( "Additional settings and tabs. May be opened or dragged onto the main window" ); };
            reserveButton.MouseLeave += Tile_MouseLeave;
            Reserve.Tiles.CollectionChanged += ( object sender, NotifyCollectionChangedEventArgs e ) => { reserveButton.KeepHidden = Reserve.Tiles.Count == 0; };

            LoadRoleModels();
        }

        #endregion C-Tor

        #region Public Methods

        public void LoadRoleModels()
        {
            VersionRoleModel.Clear();

            XmlDataProvider source = TryFindResource( "FixedPresetMenuSource" ) as XmlDataProvider;

            if( source != null )
            {
                var streaminfo = Application.GetRemoteStream( source.Source );

                XDocument document = XDocument.Load( streaminfo.Stream );

                XElement preset = document.Descendants( "preset" ).First( element => element.GetAttribute< float >( "version" ) >= MinVersion );

                foreach( XElement presetTile in preset.Descendants( "tile" ))
                {
                    Tile tile = Tile.FromXml( presetTile );

                    if( tile.Important )
                    {
                        VersionRoleModel.Add( tile.Function, tile );
                    }
                }
            }
        }

        public XElement ToXml()
        {
            XElement element = new XElement( "root" );
            element.AddAttribute( "version", MinVersion );

            XElement container = new XElement( "tilecontainer" );
            container.AddAttribute( "rows", Rows );
            container.AddAttribute( "columns", Columns );

            foreach ( Tile tile in Contents )
            {
                container.Add( tile.ToXml() );
            }

            element.Add( container );

            XElement reserveXml = Reserve.ToXml();
            if ( reserveXml != null )
            {
                element.Add( reserveXml );
            }

            return element;
        }

        private void FromXmlContainer( XElement container, double version )
        {
            int initialRows = Rows;

            Contents.Clear();
            _grid.Children.Clear();
            _grid.RowDefinitions.Clear();
            _grid.ColumnDefinitions.Clear();
            _layout.Clear();
            Rows = 0;
            Columns = 0;

            if( container != null )
            {
                int rows = container.GetAttribute<int>( "rows" );
                int columns = container.GetAttribute<int>( "columns" );

                Rows = rows;
                Columns = columns;

                IEnumerable<XElement> tileElements = container.Descendants( "tile" );

                foreach ( XElement tileElement in tileElements )
                {
                    Tile tile = Tile.FromXml( tileElement );

                    if ( version < MinVersion )
                    {
                        if ( !UpdateTileFromRoleModel( tile ) )
                        {
                            // This tile should be removed, so don't add it
                            continue;
                        }
                    }

                    tile.StringProvider = _stringProvider;

                    Contents.Add( tile );
                }
            }

            int difference = Rows - initialRows;
            HeightChanged?.Invoke( difference, difference * RowHeight.Value );
        }

        private void FromXmlReserve( XElement reserve, double version )
        {
            Reserve.Tiles.Clear();

            if( reserve != null )
            {
                IEnumerable<XElement> tileElements = reserve.Descendants( "tile" );

                foreach ( XElement tileElement in tileElements )
                {
                    Tile tile = Tile.FromXml( tileElement );

                    if ( version < MinVersion )
                    {
                        if ( !UpdateTileFromRoleModel( tile ) )
                        {
                            // This tile should be removed, so don't add it
                            continue;
                        }
                    }

                    tile.StringProvider = _stringProvider;

                    Reserve.Tiles.Add( tile );
                }
            }
        }

        public void FromXml( XElement element )
        {
            double version = element.GetAttribute<double>( "version" );

            XElement container = element.Descendants( "tilecontainer" ).First();
            FromXmlContainer( container, version );

            XElement reserve = element.Descendants( "reserve" ).FirstOrDefault();
            FromXmlReserve( reserve, version );

            if ( version < MinVersion )
            {
                Comparer comparer = new Comparer();
                var missing = VersionRoleModel.Values.Except( Contents.Concat( Reserve.Tiles ), comparer );

                foreach ( Tile roleModel in missing )
                {
                    Tile tile = new Tile()
                    {
                        Id = roleModel.Id,
                        Function = roleModel.Function,
                        FunctionGroup = roleModel.FunctionGroup,
                        Icon = roleModel.Icon,
                        StatusBindingResource = roleModel.StatusBindingResource,
                        Important = roleModel.Important
                    };

                    Reserve.Tiles.Add( tile );
                }
            }
        }

        public void RegisterClickCallback( string functionGroup, Callback.Handler handler )
        {
            _clickCallback.Register( functionGroup, handler );
        }

        public void RegisterDoubleClickCallback( string functionGroup, Callback.Handler handler )
        {
            _doubleClickCallback.Register( functionGroup, handler );
        }

        public Tile FindTile( string group, string function )
        {
            Tile found = Contents.FirstOrDefault( tile => tile.FunctionGroup == group && tile.Function == function );

            if ( found == null && Reserve != null )
            {
                found = Reserve.Tiles.FirstOrDefault( tile => tile.FunctionGroup == group && tile.Function == function );
            }

            return found;
        }

        #endregion Public Methods

        #region Private Methods

        private bool UpdateTileFromRoleModel( Tile tile )
        {
            Tile roleModel;

            if ( tile.Important )
            {
                if ( !VersionRoleModel.TryGetValue( tile.Function, out roleModel ) )
                {
                    return false;
                }

                // TODO: Id will eventually become the main identifier, rather than function, allowing all of the properties to change
                tile.Id = roleModel.Id;
                tile.Icon = roleModel.Icon;
                tile.StatusBindingResource = roleModel.StatusBindingResource;
            }

            return true;
        }

        private void AddRows( int previous, int current )
        {
            int difference = current - previous;

            if ( difference > 0 )
            {
                _layout.AddRow( difference );

                for ( int i = 0; i < difference; ++i )
                {
                    _grid.RowDefinitions.Add( new RowDefinition() { Height = RowHeight } );
                }
            }
        }

        private void AddColumns( int previous, int current )
        {
            int difference = current - previous;

            if ( difference > 0 )
            {
                _layout.AddColumn( difference );

                for ( int i = 0; i < difference; ++i )
                {
                    _grid.ColumnDefinitions.Add( new ColumnDefinition() { Width = ColumnWidth } );
                }
            }
        }

        private void CalculateCellPosition( Point point, out int row, out int column )
        {
            Point offset = _grid.TranslatePoint( new Point( 0, 0 ), this );

            row = 0;
            column = 0;
            double accumulatedHeight = offset.Y;
            double accumulatedWidth = offset.X;

            // calc row mouse was over
            foreach ( var rowDefinition in _grid.RowDefinitions )
            {
                accumulatedHeight += rowDefinition.ActualHeight;
                if ( accumulatedHeight >= point.Y )
                    break;
                ++row;
            }

            // calc col mouse was over
            foreach ( var columnDefinition in _grid.ColumnDefinitions )
            {
                accumulatedWidth += columnDefinition.ActualWidth;
                if ( accumulatedWidth >= point.X )
                    break;
                ++column;
            }
        }

        private IEnumerable<Move> CalculateDirectSwap( Move target, bool includeTarget )
        {
            int sourceCol = Grid.GetColumn( target.Tile );
            int sourceRow = Grid.GetRow( target.Tile );

            int rowShift = target.Row - sourceRow;
            int colShift = target.Column - sourceCol;

            LayoutGuide newLayout = _layout.DeepCopy();

            newLayout.RemoveTile( target.Tile );

            IEnumerable<TileImpl> tilesAtTarget = newLayout.FindTiles( target.Row, target.Column, target.RowSpan, target.ColumnSpan );

            newLayout.RemoveTiles( tilesAtTarget );

            if ( !newLayout.AddTile( target ) )
                return null;

            List<Move> swaps = new List<Move>();

            foreach ( TileImpl tile in tilesAtTarget )
            {
                Move move = new Move()
                {
                    Tile = tile,
                    Column = Grid.GetColumn( tile ) - colShift,
                    Row = Grid.GetRow( tile ) - rowShift,
                    ColumnSpan = Grid.GetColumnSpan( tile ),
                    RowSpan = Grid.GetRowSpan( tile )
                };

                swaps.Add( move );
            }

            if ( newLayout.AddTiles( swaps ) )
            {
                if ( includeTarget )
                {
                    swaps.Add( target );
                }

                return swaps;
            }

            return null;
        }

        private IEnumerable<Move> CalculateFillSwap( Move target, bool includeTarget )
        {
            int sourceCol = Grid.GetColumn( target.Tile );
            int sourceRow = Grid.GetRow( target.Tile );

            LayoutGuide newLayout = _layout.DeepCopy();

            newLayout.RemoveTile( target.Tile );

            IEnumerable<TileImpl> tilesAtTarget = newLayout.FindTiles( target.Row, target.Column, target.RowSpan, target.ColumnSpan );

            newLayout.RemoveTiles( tilesAtTarget );

            if ( !newLayout.AddTile( target ) )
                return null;

            List<Move> swaps = new List<Move>();

            foreach ( TileImpl tile in tilesAtTarget )
            {
                Move move = newLayout.AddTileToFirstFreeSpaceInArea( tile, Grid.GetRowSpan( tile ), Grid.GetColumnSpan( tile ), sourceRow, sourceCol, target.RowSpan, target.ColumnSpan );

                if ( move != null )
                {
                    swaps.Add( move );
                }
                else
                {
                    return null;
                }
            }

            if ( includeTarget )
            {
                swaps.Add( target );
            }

            return swaps;
        }

        private IEnumerable<Move> CalculateDownShift( Move target, bool includeTarget, out int rowsAdded, LayoutGuide newLayout = null )
        {
            rowsAdded = 0;

            if ( newLayout == null )
            {
                newLayout = _layout.DeepCopy();
            }

            newLayout.RemoveTile( target.Tile );

            IEnumerable<TileImpl> tilesAtTarget = newLayout.FindTiles( target.Row, target.Column, target.RowSpan, target.ColumnSpan );

            newLayout.RemoveTiles( tilesAtTarget );

            List<Move> moves = new List<Move>();

            if ( !newLayout.AddTile( target ) )
            {
                return null;
            }

            foreach ( TileImpl tile in tilesAtTarget )
            {
                int tileRow = Grid.GetRow( tile );
                int displacement = ( target.Row - tileRow ) + target.RowSpan;

                Move move = new Move()
                {
                    Tile = tile,
                    Column = Grid.GetColumn( tile ),
                    Row = tileRow + displacement,
                    ColumnSpan = Grid.GetColumnSpan( tile ),
                    RowSpan = Grid.GetRowSpan( tile )
                };

                if ( move.Row + move.RowSpan > newLayout.Rows )
                {
                    int rowsToAdd = ( ( move.Row + move.RowSpan ) - newLayout.Rows );
                    rowsAdded += rowsToAdd;
                    newLayout.AddRow( rowsToAdd );
                }

                if ( newLayout.AddTile( move ) )
                {
                    moves.Add( move );
                }
                else
                {
                    int rowsAddedRecursively;
                    IEnumerable<Move> submoves = CalculateDownShift( move, true, out rowsAddedRecursively, newLayout );

                    if ( submoves != null )
                    {
                        // Strip out any tiles that have already been moved (as they need to be moved again)
                        moves = moves.Except( submoves ).ToList();
                        moves.AddRange( submoves );
                        rowsAdded += rowsAddedRecursively;
                    }
                }
            }

            if ( includeTarget )
            {
                moves.Add( target );
            }

            return moves;
        }

        private IEnumerable<Move> MoveInternal( Move target, bool includeTarget, out int rowsAdded )
        {
            IEnumerable<Move> moves = CalculateDirectSwap( target, includeTarget );

            if ( moves == null )
            {
                moves = CalculateFillSwap( target, includeTarget );
            }

            if ( moves == null )
            {
                moves = CalculateDownShift( target, includeTarget, out rowsAdded );
            }
            else
            {
                rowsAdded = 0;
            }

            return moves;
        }

        private IEnumerable<Move> Move( Move target, bool includeTarget, out int rowsAdded )
        {
            rowsAdded = 0;

            if ( _layout.IsInRange( target.Row, target.Column, target.RowSpan, target.ColumnSpan ) )
            {
                return MoveInternal( target, includeTarget, out rowsAdded );
            }
            else if ( _layout.IsInRange( target.Row, target.Column, target.RowSpan, target.ColumnSpan, target.RowSpan ) )
            {
                LayoutGuide layout = _layout.DeepCopy();
                layout.AddRow( target.RowSpan );

                IEnumerable<Move> moves = CalculateDownShift( target, true, out rowsAdded, layout );
                rowsAdded += target.RowSpan;

                Debug.Assert( moves != null, "Supplied with a custom layout of the correct size, and instructed to include the target, CalculateDownShift() should never return null" );
                return moves;
            }

            return null;
        }

        private void PerformMoves( IEnumerable<Move> moves )
        {
            _layout.RemoveTiles( from move in moves select move.Tile );
            _layout.AddTiles( moves );

            foreach ( Move move in moves )
            {
                Grid.SetRow( move.Tile, move.Row );
                Grid.SetColumn( move.Tile, move.Column );
                Grid.SetRowSpan( move.Tile, move.RowSpan );
                Grid.SetColumnSpan( move.Tile, move.ColumnSpan );

                Tile descriptor = (Tile)move.Tile.DataContext;
                descriptor.Row = move.Row;
                descriptor.Column = move.Column;
                descriptor.RowSpan = move.RowSpan;
                descriptor.ColumnSpan = move.ColumnSpan;
            }
        }

        private void AnimateDisplacedTiles( Point point, TileImpl draggedTile )
        {
            int row, column;
            CalculateCellPosition( point, out row, out column );
            row -= _mouseRowOffset;
            column -= _mouseColumnOffset;

            if ( row != _previousDragRow || column != _previousDragColumn )
            {
                foreach ( TileImpl tile in _displacedTiles )
                {
                    tile.AnimateTo( 0, 0 );
                }

                _displacedTiles.Clear();

                _previousDragRow = row;
                _previousDragColumn = column;

                Move dragTargetMove = new Move()
                {
                    Tile = draggedTile,
                    Row = row,
                    Column = column,
                    RowSpan = Grid.GetRowSpan( draggedTile ),
                    ColumnSpan = Grid.GetColumnSpan( draggedTile )
                };

                int rowsAdded;
                IEnumerable<Move> moves = Move( dragTargetMove, false, out rowsAdded );

                if ( moves != null )
                {
                    foreach ( Move move in moves )
                    {
                        double rowAnimationDistance = ( move.Row - Grid.GetRow( move.Tile ) ) * RowHeight.Value;
                        double columnAnimationDistance = ( move.Column - Grid.GetColumn( move.Tile ) ) * ColumnWidth.Value;


                        move.Tile.AnimateTo( columnAnimationDistance, rowAnimationDistance );
                        _displacedTiles.Add( move.Tile );
                    }
                }

                // Since the animation would animate to a potential new row, we will need to pretend that the grid has grown to
                // accommodate it and notify our listeners (otherwise, the animation may be obscured by other controls if our listeners don't react)
                if ( _temporaryRowsAddedByAnimation < rowsAdded )
                {
                    int difference = rowsAdded - _temporaryRowsAddedByAnimation;
                    HeightChanged?.Invoke( difference, difference * RowHeight.Value );
                    _temporaryRowsAddedByAnimation = rowsAdded;
                }
            }
        }

        private TileImpl CreateTileImpl( Tile data )
        {
            TileImpl tile = new TileImpl() { DataContext = data };

            tile.Click += Tile_Click;
            tile.DoubleClick += Tile_DoubleClick;
            tile.RequestResize += Tile_RequestResize;
            tile.RequestDelete += Tile_RequestDelete;
            tile.MouseEnter += Tile_MouseEnter;
            tile.MouseLeave += Tile_MouseLeave;

            object presetMenu = TryFindResource( "PresetMenu" );

            if ( presetMenu != null )
            {
                tile.PrependedMenuItems.Add( presetMenu );
            }

            SetTileIconSize( tile, data );

            if ( data.StatusBindingResource != null )
            {
                Binding statusBinding = TryFindResource( data.StatusBindingResource ) as Binding;

                if ( statusBinding != null )
                {
                    tile.SetBinding( TileImpl.StatusProperty, statusBinding );
                }
            }

            return tile;
        }

        private void SetTileIconSize( TileImpl tile, Tile data )
        {
            double potentialHeight = ( ( data.RowSpan + data.ColumnSpan - 1 ) * ( IconSizeIncrement ) ) - ( tile.Margin.Left * ( data.ColumnSpan - data.RowSpan ) );
            tile.IconHeight = potentialHeight;
        }

        private void ShowTooltip( string tooltip )
        {
            TooltipDescription = tooltip;

            DoubleAnimation anim = new DoubleAnimation( 1, TimeSpan.FromSeconds( 0.1 ) );
            BeginAnimation( TooltipOpacityProperty, anim );
        }

        private void HideTooltip()
        {
            DoubleAnimation anim = new DoubleAnimation( 0, TimeSpan.FromSeconds( 0.1 ) );
            BeginAnimation( TooltipOpacityProperty, anim );
        }

        #endregion Private Methods

        #region Event Handlers

        private void Tile_MouseEnter( object sender, MouseEventArgs e )
        {
            TileImpl tileImpl = (TileImpl)sender;
            Tile tile = (Tile)tileImpl.DataContext;

            ShowTooltip( tile.Description );
        }

        private void Tile_MouseLeave( object sender, MouseEventArgs e )
        {
            HideTooltip();
        }

        private void LoadPreset( object sender, ExecutedRoutedEventArgs e )
        {
            System.Xml.XmlElement element = (System.Xml.XmlElement)e.Parameter;
            XElement converted = XElement.Parse( element.OuterXml );
            FromXml( converted );
        }

        private async void SavePreset( object sender, ExecutedRoutedEventArgs e )
        {
            NewPresetDialog dialog = new NewPresetDialog() { Owner = Window.GetWindow( this ) };

            if( await dialog.ShowAsync() )
            {
                XElement preset = ToXml();

                preset.Name = "preset";
                preset.SetAttributeValue( "name", dialog.Name );
                preset.AddAttribute( "icon", dialog.Icon );

                try
                {
                    XDocument presets = XDocument.Load( UserPresetsLocation );
                    presets.Root.Add( preset );
                    XmlHelper.Save( presets.Root, UserPresetsLocation );
                }
                catch ( System.IO.FileNotFoundException )
                {
                    XElement presets = new XElement( "presets" );
                    presets.Add( preset );
                    XmlHelper.Save( presets, UserPresetsLocation );
                }

                XmlDataProvider presetSource = TryFindResource( "UserPresetMenuSource" ) as XmlDataProvider;
                presetSource.Refresh();
            }
        }

        private static void SetColumns( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Container container = (Container)d;
            container.AddColumns( (int)e.OldValue, (int)e.NewValue );
        }

        private static void SetRows( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Container container = (Container)d;
            container.AddRows( (int)e.OldValue, (int)e.NewValue );
        }

        private void Contents_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Add )
            {
                foreach ( Tile data in e.NewItems )
                {
                    TileImpl tile = CreateTileImpl( data );

                    Grid.SetRow( tile, data.Row );
                    Grid.SetColumn( tile, data.Column );
                    Grid.SetRowSpan( tile, data.RowSpan );
                    Grid.SetColumnSpan( tile, data.ColumnSpan );

                    _grid.Children.Add( tile );

                    Move move = new Move()
                    {
                        Tile = tile,
                        Column = data.Column,
                        Row = data.Row,
                        ColumnSpan = data.ColumnSpan,
                        RowSpan = data.RowSpan
                    };

                    _layout.AddTile( move );
                }
            }
            else if ( e.Action == NotifyCollectionChangedAction.Remove )
            {
                foreach ( Tile data in e.OldItems )
                {
                    foreach ( TileImpl tile in _grid.Children )
                    {
                        if ( tile.DataContext.Equals( data ) )
                        {
                            _grid.Children.Remove( tile );
                            _layout.RemoveTile( tile );
                            tile.Click -= Tile_Click;
                            tile.DoubleClick -= Tile_DoubleClick;
                            tile.RequestResize -= Tile_RequestResize;
                            tile.RequestDelete -= Tile_RequestDelete;
                            tile.DataContext = null;

                            break;
                        }
                    }
                }
            }
        }

        private void Tile_RequestResize( TileImpl impl, int rowspan, int columnspan )
        {
            int row = Grid.GetRow( impl );
            int column = Grid.GetColumn( impl );

            if ( row + rowspan > Rows )
            {
                row -= ( row + rowspan ) - Rows;
            }

            if ( column + columnspan > Columns )
            {
                column -= ( column + columnspan ) - Columns;
            }

            Move move = new Move()
            {
                Tile = impl,
                Row = row,
                Column = column,
                RowSpan = rowspan,
                ColumnSpan = columnspan
            };

            int rowsAdded;
            IEnumerable<Move> moves = Move( move, true, out rowsAdded );
            Rows += rowsAdded;

            if ( moves != null )
            {
                PerformMoves( moves );
                SetTileIconSize( impl, (Tile)impl.DataContext );

                PostDragCleanup( rowsAdded );
            }
        }

        private void Tile_Click( Tile tile, TileImpl impl )
        {
            _clickCallback.Invoke( tile.FunctionGroup, tile.Function );
        }

        private void Tile_DoubleClick( Tile tile, TileImpl impl )
        {
            _doubleClickCallback.Invoke( tile.FunctionGroup, tile.Function );
        }

        private void Tile_RequestDelete( Tile tile, TileImpl impl )
        {
            Contents.Remove( tile );
            PostDragCleanup( 0 );

            if ( tile.Important )
            {
                Reserve.Tiles.Add( tile );
            }
        }

        #endregion Event Handlers

        #region IDragSource

        public DragDropEffects GetDragObject( Point point, out DependencyObject dragObject, out object dragData )
        {
            int row, column;
            CalculateCellPosition( point, out row, out column );

            if ( _layout.IsInRange( row, column, 1, 1 ) )
            {
                Debug.Print( "Container click: {0}, {1}", row, column );

                TileImpl tile = _layout[ row ][ column ];

                if ( tile != null )
                {
                    _mouseRowOffset = row - Grid.GetRow( tile );
                    _mouseColumnOffset = column - Grid.GetColumn( tile );

                    dragObject = tile;
                    dragData = tile.DataContext;
                    return DragDropEffects.Move;
                }
            }

            dragObject = null;
            dragData = null;
            return DragDropEffects.None;
        }

        public void OnDragStart( DragData data )
        {
            TileImpl tile = data.DraggedObject as TileImpl;

            if ( tile != null )
            {
                Panel.SetZIndex( tile, 1 );
            }
        }

        public void OnDragStop( DragData data, DragDropEffects mode )
        {
            if ( mode == DragDropEffects.None )
            {
                OnDragCancelled( mode, data );
            }

            TileImpl tile = data.DraggedObject as TileImpl;

            if ( tile != null )
            {
                Panel.SetZIndex( tile, 0 );
            }
        }

        #endregion IDragSource

        #region IDragTarget

        private TileImpl CreateTileFromSource( DragData data )
        {
            TileImpl impl = data.UserData as TileImpl;

            if ( impl == null )
            {
                Tile tile = data.UserData as Tile;

                if ( tile == null )
                {
                    IDragCreateViewModel creator = data.UserData as IDragCreateViewModel;

                    if ( creator != null )
                    {
                        tile = (Tile)creator.CreateDragViewModel( typeof( Tile ) );
                    }
                }

                if ( tile != null )
                {
                    impl = CreateTileImpl( tile );

                    Contents.AddSilent( tile );
                    _grid.Children.Add( impl );

                    Grid.SetRowSpan( impl, tile.RowSpan );
                    Grid.SetColumnSpan( impl, tile.ColumnSpan );

                    data.UserData = impl;
                }
            }

            return impl;
        }

        public bool OnDragOver( Point currentPosition, Point startPosition, DragDropEffects mode, DragData data )
        {
            TileImpl impl = data.DraggedObject as TileImpl;

            if ( impl == null )
            {
                impl = CreateTileFromSource( data );

                startPosition.X = ColumnWidth.Value * Grid.GetColumnSpan( impl ) * 0.5;
                startPosition.Y = RowHeight.Value * Grid.GetRowSpan( impl ) * 0.5;
            }

            if ( impl != null )
            {
                double xOffset = startPosition.X - impl.LayoutTransform.Value.OffsetX;
                double yOffset = startPosition.Y - impl.LayoutTransform.Value.OffsetY;

                impl.RenderAt( currentPosition.X - xOffset, currentPosition.Y - yOffset );

                AnimateDisplacedTiles( currentPosition, impl );

                return true;
            }

            return false;
        }

        private void PostDragCleanup( int rowsAdded )
        {
            int rowsTrimmed = _layout.TrimRows();
            rowsAdded -= rowsTrimmed;
            Rows -= rowsTrimmed;

            // Notify any listeners of any change in height that happened as a result of this drag/drop operation
            int notificationDifference = rowsAdded - _temporaryRowsAddedByAnimation;

            if ( notificationDifference != 0 )
            {
                HeightChanged?.Invoke( notificationDifference, notificationDifference * RowHeight.Value );
            }

            int totalChange = rowsAdded + _temporaryRowsAddedByAnimation;
            if ( totalChange < 0 )
            {
                _grid.RowDefinitions.RemoveRange( _grid.RowDefinitions.Count + totalChange, -totalChange );
            }

            _temporaryRowsAddedByAnimation = 0;

            foreach ( TileImpl displacedTile in _displacedTiles )
            {
                displacedTile.ClearAnimation();
            }

            _displacedTiles.Clear();
        }

        public DragDropEffects OnDrop( Point point, DragDropEffects mode, DragData data )
        {
            TileImpl tile = data.DraggedObject as TileImpl;

            if ( tile == null )
            {
                tile = data.UserData as TileImpl;
            }

            if ( tile != null )
            {
                int row, column;
                CalculateCellPosition( point, out row, out column );

                row -= _mouseRowOffset;
                column -= _mouseColumnOffset;

                Move dragTargetMove = new Move()
                {
                    Tile = tile,
                    Row = row,
                    Column = column,
                    RowSpan = Grid.GetRowSpan( tile ),
                    ColumnSpan = Grid.GetColumnSpan( tile )
                };

                int rowsAdded;
                IEnumerable<Move> moves = Move( dragTargetMove, true, out rowsAdded );
                Rows += rowsAdded;

                if ( moves != null )
                {
                    PerformMoves( moves );
                    PostDragCleanup( rowsAdded );

                    tile.ClearAnimation();

                    Debug.Print( "Dropped tile at: {0}, {1}", row, column );
                    _layout.Print();

                    return mode;
                }
            }

            return DragDropEffects.None;
        }

        public void OnDragCancelled( DragDropEffects mode, DragData data )
        {
            // For newly created tiles
            TileImpl tile = data.UserData as TileImpl;

            if ( tile != null )
            {
                Contents.Remove( (Tile)tile.DataContext );
            }

            // For existing tiles
            if ( mode == DragDropEffects.Move )
            {
                tile = data.DraggedObject as TileImpl;

                if ( tile != null )
                {
                    tile.ClearAnimation();
                }
            }

            PostDragCleanup( 0 );
        }

        #endregion IDragTarget

        #region Events

        public delegate void HeightChangedHandler( int difference, double sizeDifference );
        public event HeightChangedHandler HeightChanged;

        #endregion Events
    }
}
